var rnothtmlwhite = (/[^\x20\t\r\n\f]+/g);

// Convert String-formatted options into Object-formatted ones
function createOptions(options) {
  var object = {};
  (options.match(rnothtmlwhite) || []).forEach((flag) => {
    object[ flag ] = true;
  });
  return object;
}

function inArray(elem, arr, i) {
  return (arr === null || arr === undefined) ? -1 : [].indexOf.call(arr, elem, i);
}

/*
 * Create a callback list using the following parameters:
 *
 *	options: an optional list of space-separated options that will change how
 *			the callback list behaves or a more traditional option object
 *
 * By default a callback list will act like an event callback list and can be
 * "fired" multiple times.
 *
 * Possible options:
 *
 *	once:			will ensure the callback list can only be fired once (like a Promise)
 *
 *	memory:			will keep track of previous values and will call any callback added
 *					after the list has been fired right away with the latest "memorized"
 *					values (like a Promise)
 *
 *	unique:			will ensure a callback can only be added once (no duplicate in the list)
 *
 *	stopOnFalse:	interrupt callings when a callback returns false
 *
 */
export const Callbacks = function (options) {

  // Convert options from String-formatted to Object-formatted if needed
  // (we check in cache first)
  options = typeof options === "string" ?
    createOptions(options) :
    Object.assign({}, options);

  var // Flag to know if list is currently firing
    firing,

    // Last fire value for non-forgettable lists
    memory,

    // Flag to know if list was already fired
    fired,

    // Flag to prevent firing
    locked,

    // Actual callback list
    list = [],

    // Queue of execution data for repeatable lists
    queue = [],

    // Index of currently firing callback (modified by add/remove as needed)
    firingIndex = -1,

    // Fire callbacks
    fire = function () {

      // Enforce single-firing
      locked = locked || options.once;

      // Execute callbacks for all pending executions,
      // respecting firingIndex overrides and runtime changes
      fired = firing = true;
      for (; queue.length; firingIndex = -1) {
        memory = queue.shift();
        while (++firingIndex < list.length) {

          // Run callback and check for early termination
          if (list[ firingIndex ].apply(memory[ 0 ], memory[ 1 ]) === false &&
            options.stopOnFalse) {

            // Jump to end and forget the data so .add doesn't re-fire
            firingIndex = list.length;
            memory = false;
          }
        }
      }

      // Forget the data if we're done with it
      if (!options.memory) {
        memory = false;
      }

      firing = false;

      // Clean up if we're done firing for good
      if (locked) {

        // Keep an empty list if we have data for future add calls
        if (memory) {
          list = [];

        // Otherwise, this object is spent
        } else {
          list = "";
        }
      }
    },

    // Actual Callbacks object
    self = {

      getList: function (num) {
        // Return all the elements in list
        if (num === undefined) {
          return [].slice.call(list);
        }

        // Return just the one element from the set
        return num < 0 ? this[ num + this.length ] : list[ num ];
      },

      // Add a callback or a collection of callbacks to the list
      add: function () {
        if (list) {

          // If we have memory from a past run, we should fire after adding
          if (memory && !firing) {
            firingIndex = list.length - 1;
            queue.push(memory);
          }

          (function add(args) {
            for (const _ in args) {
              if (args.hasOwnProperty(_)) {
                const arg = args[ _ ];
                if (Object.prototype.toString.call(arg) === '[object Function]') {
                  if (!options.unique || !self.has(arg)) {
                    list.push(arg);
                  }
                } else if (arg && arg.length && Object.prototype.toString.call(arg) !== "[object String]") {

                  // Inspect recursively
                  add(arg);
                }
              }
            }
          })(arguments);

          if (memory && !firing) {
            fire();
          }
        }
        return this;
      },

      // Remove a callback from the list
      remove: function () {
        for (const _ in arguments) {
          if (arguments.hasOwnProperty(_)) {
            const arg = arguments[ _ ];
            var _index;
            while ((_index = inArray(arg, list, _index)) > -1) {
              list.splice(_index, 1);

              // Handle firing indexes
              if (_index <= firingIndex) {
                firingIndex--;
              }
            }
          }
        }
        return this;
      },

      has: function (fn) {
        return fn ? inArray(fn, list) > -1 : list.length > 0;
      },

      // Remove all callbacks from the list
      empty: function () {
        if (list) { list = []; }
        return this;
      },

      disable: function () {
        locked = queue = [];
        list = memory = "";
        return this;
      },

      disabled: function () {
        var _list = !list;
        return _list;
      },

      lock: function () {
        locked = queue = [];
        if (!memory && !firing) { list = memory = ""; }
        return this;
      },

      locked: function () {
        var _locked = !!locked;
        return _locked;
      },

      fireWith: function (context, args) {
        if (!locked) {
          args = args || [];
          args = [ context, args.slice ? args.slice() : args ];
          queue.push(args);
          if (!firing) { fire(); }
        }
        return this;
      },

      fire: function () {
        self.fireWith(this, arguments);
        return this;
      },

      fired: function () {
        var _fired = !!fired;
        return _fired;
      }
    };

  return self;
};